Android MVP 详解(下)

作者:李旺成
时间:2016年4月3日


上篇

5. 最佳实践

好了终于要点讲自己的东西了,有点小激动。下面这些仅表示个人观点,非一定之规,各位看官按需取用,有说的不对的,敬请谅解。关于命名规范可以参考我的另一篇文章“Android 编码规范”。老规矩先上图:

MVPBestPractice 思维导图
在参考了 kenjuwagatsumaMVP Architecture in Android DevelopmentSaúl MolineroA useful stack on android #1, architecture 之后,我决定采用如下的分层方案来构建这个演示Demo,如下:
分层架构方案
总体架构可以被分成四个部分 :
Presentation:负责展示图形界面,并填充数据,该层囊括了 View 和 Presenter (上图所示的Model我理解为 ViewModel – 为 View 提供数据的 Model,或称之为 VO – View Object)。

Domain:负责实现app的业务逻辑,该层中由普通的Java对象组成,一般包括 Usecases 和 Business Logic。

Data:负责提供数据,这里采用了 Repository 模式,Repository 是仓库管理员,Domain 需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。Android 开发中常见的数据来源有,RestAPI、SQLite数据库、本地缓存等。

Library:负责提供各种工具和管理第三方库,现在的开发一般离不开第三方库(当然可以自己实现,但是不要重复造轮子不是吗?),这里建议在统一的地方管理(那就是建一个单独的 module),尽量保证和 Presentation 层分开。

AndroidStudio 中构建项目

5.1. 关于包结构划分

一个项目是否好扩展,灵活性是否够高,包结构的划分方式占了很大比重。很多项目里面喜欢采用按照特性分包(就是Activity、Service等都分别放到一个包下),在模块较少、页面不多的时候这没有任何问题;但是对于模块较多,团队合作开发的项目中,这样做会很不方便。所以,我的建议是按照模块划分包结构

其实这里主要是针对 Presentation 层了,这个演示 Demo 我打算分为四个模块:登录,首页,查询天气和我的(这里仅仅是为了演示需要,具体如何划分模块还得根据具体的项目,具体情况具体分析了)。

划分好包之后如下图所示:
包结构划分

5.2. 关于res拆分

功能越来越多,项目越做越大,导致资源文件越来越多,虽然通过命名可以对其有效归类(如:通过添加模块名前缀),但文件多了终究不方便。得益于 Gradle,我们也可以对 res 目录进行拆分,先来看看拆分后的效果:
按模块拆分 res 目录
注意:resource 目录的命名纯粹是个人的命名偏好,该目录的作用是用来存放那些不需要分模块放置的资源。
res 目录的拆分步骤如下:
1) 首先打开 module 的 build.gradle 文件
res 拆分 Step1
2) 定位到 defaultConfig {}buildTypes {} 之间
res 拆分 Step2
3) 在第二步定位处编辑输入 sourceSets {} 内容,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java','.apt_generated']
aidl.srcDirs = ['src/main/aidl','.apt_generated']
assets.srcDirs = ['src/main/assets']
res.srcDirs =
[
'src/main/res/home',
'src/main/res/login',
'src/main/res/mine',
'src/main/res/weather',
'src/main/res/resource',
'src/main/res/'

]
}
}

4) 在 res 目录下按照 sourceSets 中的配置建立相应的文件夹,将原来 res 下的所有文件(夹)都移动到 resource 目录下,并在各模块中建立 layout 等文件夹,并移入相应资源,最后 Sync Project 即可。

5.3. 怎么写 Model

这里的 Model 其实贯穿了我们项目中的三个层,Presentation、Domain 和 Data。暂且称之为 Model 吧,这也我将提供 Repository 功能的层称之为 Data Layer 的缘故(有些称这一层为 Model Layer)。

首先,谈谈我对于 Model 是怎么理解的

应用都离不开数据,而这些数据来源有很多,如网络、SQLite、文件等等。一个应用对于数据的操作无非就是:获取数据、编辑(修改)数据、提交数据、展示数据这么几类。从分层的思想和 JavaEE 开发中积累的经验来看,我觉得 Model 中的类需要分类。从功能上来划分,可以分出这么几类:

VO(View Object)
视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object)
数据传输对象,这个概念来源于 JavaEE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
DO(Domain Object)
领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
PO(Persistent Object)
持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。

注意:关于vo、dto、do、po可以参考这篇文章-“领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处

当然这些不一定都存在,这里只是列举一下,可以有这么多分类,当然列举的也不全。

其次,要搞清楚 Domain 层和 Data 层分别是用来做什么的

知道 Domain 层和 Data 层的作用,然后才知道哪些 Model 该往 Data 层中写,哪些该往 Domain 层中写。
Data 层负责提供数据。
Data 层不会知道任何关于 Domain 和 Presentation 的数据。它可以用来实现和数据源(数据库,REST API或者其他源)的连接或者接口。这个层面同时也实现了整个app所需要的实体类。

Domain 层相对于 Presentation 层完全独立,它会实现应用的业务逻辑,并提供 Usecases。
Presentation 从 Domain 层获取到的数据,我的理解就是 VO 了,VO 应该可以直接使用。

注意:这里说的直接使用是指不需要经过各种转换,各种判断了,如 Activity 中某个控件的显示隐藏是根据 VO 中的 visibility 字段来决定,那么这个最好将 visibility 作为 int 型,而且,取值为VISIBLE/INVISIBLE/GONE,或者至少是 boolean 型的。

注意:这里所谓的业务逻辑可能会于 Presenter 的功能概念上有点混淆。打个比方,假如 usecase 接收到的是一个 json 串,里面包含电影的列表,那么把这个 json 串转换成 json 以及包装成一个 ArrayList,这个应当是由 usecase 来完成。而假如 ArrayList 的 size 为0,即列表为空,需要显示缺省图,这个判断和控制应当是由 Presenter 完成的。(上述观点参考自:Saúl Molinero

最后,就是关于 Data 层,采用的 Repository 模式,建议抽象出接口来,Domain 层需要感知数据是从哪里取出来的。

5.4. 怎么写 View

先区分一下Android View、View、界面的区别
Android View
指的是继承自android.view.View的Android组件。
View
接口和实现类,接口部分用于由 Presenter 向 View 实现类通信,可以在 Android 组件中实现它。一般最好直接使用 Activity,Fragment 或自定义 View。
界面
界面是面向用户的概念。比如要在手机上进行界面间切换时,我们在代码中可以通过多种方式实现,如 Activity 到 Activity 或一个 Activity 内部的 Fragment/View 进行切换。所以这个概念基于用户的视觉,包括了所有 View 中能看到的东西。

那么该怎么写 View 呢?

在 MVP 中 View 是很薄的一层,里面不应该有业务逻辑,所以一般只提供一些 getter 和 setter 方法,供 Presenter 操作。关于 View,我有如下建议:

  1. 简单的页面中直接使用 Activity/Fragment 作为 View 的实现类,然后抽取相应的接口
  2. 在一些有 Tab 的页面中,可以使用 Activity + Fragment ( + ViewPager) 的方式来实现,至于 ViewPager,视具体情况而定,当然也可以直接 Activity + ViewPager 或者其他的组合方式
  3. 在一些包含很多控件的复杂页面中,那么建议将界面拆分,抽取自定义 View,也就是一个 Activity/Fragment 包含多个 View(实现多个 View 接口)

5.5. 怎么写 Presenter

Presenter 是 Android MVP 实现中争论的焦点,上篇中介绍了多种“MVP 框架”,其实都是围绕着Presenter应该怎么写

有一篇专门介绍如何设计 Presenter 的文章(Modeling my presentation layer),个人感觉写得不错,这里借鉴了里面不少的观点,感兴趣的童鞋可以去看看。下面进入正题。

为什么写 Presenter 会这么纠结,我认为主要有以下几个问题:

  1. 我们将 Activity/Fragment 视为 View,那么 View 层的编写是简单了,但是这有一个问题,当手机的状态发生改变时(比如旋转手机)我们应该如何处理Presenter对象,那也就是说 Presenter 也存在生命周期,并且还要“手动维护”(别急,这是引起来的,下面会细说)
  2. Presenter 中应该没有 Android Framework 的代码,也就是不需要导 Framework 中的包,那么问题来了,页面跳转,显示对话框这些情况在 Presenter 中该如何完成
  3. 上面说 View 的时候提到复杂的页面建议通过抽取自定义 View 的方式,将页面拆分,那么这个时候要怎么建立对应的 Presenter 呢
  4. View 接口是可以有多个实现的,那我们的 Presenter 该怎么写呢

好,现在我将针对上面这些问题一一给出建议。

5.5.1. 关于 Presenter 生命周期的问题

先看图(更详细讲解可以看看这篇文章Presenter surviving orientation changes with Loaders
Presenter生命周期
如上图所示,方案1和方案2都不够优雅(这也是很多“MVP 框架”采用的实现方案),而且并不完善,只适用于一些场景。而方案3,让人耳目一新,看了之后不禁想说 Loader 就是为 Presenter 准备的啊。这里我们抓住几个关键点就好了:

  • Loader 是 Android 框架中提供的
  • Loader 在手机状态改变时是不会被销毁
  • Loader 的生命周期是是由系统控制的,会在Activity/Fragment不再被使用后由系统回收
  • Loader 与 Activity/Fragment 的生命周期绑定,所以事件会自己分发
  • 每一个 Activity/Fragment 持有自己的 Loader 对象的引用
  • 具体怎么用,在 Antonio Gutierrez 的文章已经阐述的很明白,我就不再赘述了

好吧,我有一点要补充,上面说的方案1和方案2不是说就没有用了,还是视具体情况而定,如果没有那么多复杂的场景,那么用更简单的方案也未尝不可。能解决问题就好,不要拘泥于这些条条框框…(话说,咱这不是为了追求完美吗,哈哈)

5.5.2. 关于页面跳转和显示Dialog

首先,说说页面跳转,前一阵子忙着重构公司的项目,发现项目中很多地方使用 startActivity() 和使用 Intent 的 putExtra() 显得很乱;更重要的是从 Intent 中取数据的时候需要格外小心——类型要对应,key 要写对,不然轻则取不到数据,重则 Crash。

还有一点,就是当前 Activity/Fragment 必须要知道目标 Activity 的类名,这里耦合的很严重,有没有。当时就在想这是不是应该封装一下啊,或者有更好的解决方案。于是,先在网上搜了一下,知乎上有类似的提问,有人建议写一个 Activity Router(Activity 路由表)。嗯,正好和我的思路类似,那就开干。

我的思路很简单,在 util 包中定义一个 NavigationManager 类,在该类中按照模块使用注释先分好区块(为什么要分区块,去看看我的 “Android 编码规范”)。然后为每个模块中的 Activity 该如何跳转,定义一个静态方法。

如果不需要传递数据的,那就很简单了,只要传入调用者的 Context,直接 new 出 Intent,调用该 Context 的 startActivity() 方法即可。
代码如下:
导航管理类-跳转系统页面
导航管理类-跳转不需要传递数据的页面

如果需要传递数据呢?刚才说了,使用 Bundle 或者 putExtra() 这种方式很不优雅,而且容易出错(那好,你个给优雅的来看看,哈哈)。确实,我没想到比较优雅的方案,在这里我提供一个粗糙的方案,仅供大家参考一下,如有你有更好的,那麻烦也和我分享下。

我的方案是这样的,使用序列化对象来传递数据(建议使用 Parcelable,不要偷懒去用 Serializable,这个你懂的)。为需要传递数据的 Activity 新建一个实现了 Parcelable 接口的类,将要传递的字段都定义在该类中。其他页面需要跳转到该 Activity,那么就需要提供这个对象。在目标 Activity 中获取到该对象后,那就方便了,不需要去找对应的 key 来取数据了,反正只要对象中有的,你就能直接使用。

注意:这里我建议将序列化对象中的所有成员变量都定义为 public 的,一来,可以减少代码量,主要是为了减少方法数(虽说现在对于方法数超 64K 有比较成熟的 dex 分包方案,但是尽量不超不是更好);二来,通过对象的 public 属性直接读写比使用 getter/setter 速度要快(听说的,没有验证过)。

注意:这里建议在全局常量类(没有,那就定义一个,下面会介绍)中定义一个唯一的 INTENT_EXTRA_KEY,往 Bundle 中存和取得时候都用它,也不用去为命名 key 费神(命名从来不简单,不是吗),取的时候也不用思考是用什么 key 存的,简单又可以避免犯错。

具体如下图所示:
导航管理类-跳转需要传递数据的页面
导航管理类-传递数据
导航管理类-获取传递的数据
导航管理类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//==========逻辑方法==========
public static <T> T getParcelableExtra(Activity activity) {
Parcelable parcelable = activity.getIntent().getParcelableExtra(NavigateManager.PARCELABLE_EXTRA_KEY);
activity = null;
return (T)parcelable;
}

private static void overlay(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
setFlags(intent, flags);
putParcelableExtra(intent, parcelable);
context.startActivity(intent);
context = null;
}

private static void overlay(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
putParcelableExtra(intent, parcelable);
context.startActivity(intent);
context = null;
}

private static void overlay(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
Intent intent = new Intent(context, targetClazz);
putSerializableExtra(intent, serializable);
context.startActivity(intent);
context = null;
}

private static void overlay(Context context, Class<? extends Activity> targetClazz) {
Intent intent = new Intent(context, targetClazz);
context.startActivity(intent);
context = null;
}

private static void forward(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
setFlags(intent, flags);
intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
context.startActivity(intent);
if (isActivity(context)) return;
((Activity)context).finish();
context = null;
}

private static void forward(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
putParcelableExtra(intent, parcelable);
context.startActivity(intent);
if (isActivity(context)) return;
((Activity)context).finish();
context = null;
}

private static void forward(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
Intent intent = new Intent(context, targetClazz);
putSerializableExtra(intent, serializable);
context.startActivity(intent);
if (isActivity(context)) return;
((Activity)context).finish();
context = null;
}

private static void forward(Context context, Class<? extends Activity> targetClazz) {
Intent intent = new Intent(context, targetClazz);
context.startActivity(intent);
if (isActivity(context)) return;
((Activity)context).finish();
context = null;
}

private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags) {
Intent intent = new Intent(context, targetClazz);
if (isActivity(context)) return;
((Activity)context).startActivityForResult(intent, flags);
context = null;
}

private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
if (isActivity(context)) return;
putParcelableExtra(intent, parcelable);
((Activity)context).startActivityForResult(intent, flags);
context = null;
}

private static void setResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
Intent intent = new Intent(context, targetClazz);
setFlags(intent, flags);
putParcelableExtra(intent, parcelable);
if (isActivity(context)) return;
((Activity)context).setResult(flags, intent);
((Activity)context).finish();
}

private static boolean isActivity(Context context) {
if (!(context instanceof Activity)) return true;
return false;
}

private static void setFlags(Intent intent, int flags) {
if (flags < 0) return;
intent.setFlags(flags);
}

private static void putParcelableExtra(Intent intent, Parcelable parcelable) {
if (parcelable == null) return;
intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
}

private static void putSerializableExtra(Intent intent, Serializable serializable) {
if (serializable == null) return;
intent.putExtra(PARCELABLE_EXTRA_KEY, serializable);
}

传递数据用的序列化对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class DishesStockVO implements Parcelable {

public boolean isShowMask;
public int pageNum;

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte(isShowMask ? (byte) 1 : (byte) 0);
dest.writeInt(this.pageNum);
}

public DishesStockVO() {
}

protected DishesStockVO(Parcel in) {
this.isShowMask = in.readByte() != 0;
this.pageNum = in.readInt();
}

public static final Creator<DishesStockVO> CREATOR = new Creator<DishesStockVO>() {
public DishesStockVO createFromParcel(Parcel source) {
return new DishesStockVO(source);
}

public DishesStockVO[] newArray(int size) {
return new DishesStockVO[size];
}
};

@Override
public String toString() {
return "DishesStockVO{" +
"isShowMask=" + isShowMask +
", pageNum=" + pageNum +
'}';
}
}

好像,还没入正题。这里再多说一句,beautifulSoup 写了一篇文章,说的就是 Android 路由表框架的,可以去看看——“Android路由框架设计与实现”。

好了,回到主题,在 Presenter 中该如何处理页面跳转的问题。在这里我建议简单处理,在 View Interface 中定义好接口(方法),在 View 的实现类中去处理(本来就是它的责任,不是吗?)。在 View 的实现类中,使用 NavigationManager 工具类跳转,达到解耦的目的。
如下图所示:
对页面跳转的处理

显示对话框
我在这里采用和页面跳转的处理类似的方案,这也是 View 的责任,所以让 View 自己去完成。这里建议每个模块都定义一个相应的 XxxDialogManager 类,来管理该模块所有的弹窗,当然对于弹窗本来就不多的,那就直接在 util 包中定义一个 DialogManager 类就好了。
如下图:
对显示对话框的处理

5.5.3. 一个页面多个View的问题

对于复杂页面,一般建议拆成多个自定义 View,那么这就引出一个问题,这时候是用一个 Presenter 好,还是定义多个 Presenter 好呢?
我的建议是,每个 View Interface 对应一个 Presenter,如下图所示:
一个页面多个 View 处理

5.5.4. 一个View有两个实现类的问题

有些时候会遇到这样的问题,只是展示上有差别,两个页面上所有的操作都是一样的,这就意味着 View Interface 是一样的,只是有两个实现类。

这个问题该怎么处理,或许可以继续使用同样的Presenter并在另一个Android组件中实现View接口。不过这个界面似乎有更多的功能,那要不要把这些新功能加进这个Presenter呢?

这个视情况而定,有多种方案:一是将Presenter整合负责不同操作,二是写两个Presenter分别负责操作和展示,三是写一个Presenter包含所有操作(在两个View相似时)。记住没有完美的解决方案,编程的过程就是让步的过程。(参考自:Christian Panadero PaNaVTECModeling my presentation layer
如下图所示:
一个 View 多个实现类处理

5.6. 关于 RestAPI

一般项目当中会用到很多和服务器端通信用的接口,这里建议在每个模块中都建立一个 api 包,在该包下来统一处理该模块下所有的 RestAPI。
如下图所示:
统一管理 RestAPI

对于网络请求之类需要异步处理的情况,一般都需要传入一个回调接口,来获取异步处理的结果。对于这种情况,我建议参考 onClick(View v) {} 的写法。那就是为每一个请求编一个号(使用 int 值),我称之为 taskId,可以将该编号定义在各个模块的常量类中。然后在回调接口的实现类中,可以在回调方法中根据 taskId 来统一处理(一般是在这里分发下去,分别调用不同的方法)。
如下图所示:
定义 taskId
异步任务回调处理

5.7. 关于项目中的常量管理

Android 中不推荐使用枚举推荐使用常量,我想说说项目当中我一般是怎么管理常量的。

灵感来自 R.java 类,这是由项目构建工具自动生成并维护的,可以进去看看,里面是一堆的静态内部类,如下图:
Android 中的 R 文件

看到这,可能大家都猜到了,那就是定义一个类来管理全局的常量数据,我一般喜欢命名为 C.java。这里有一点要注意,我们的项目是按模块划分的包,所以会有一些是该模块单独使用的常量,那么这些最好不要写到全局常量类中,否则会导致 C 类膨胀,不利于管理,最好是将这些常量定义到各个模块下面。如下图所示:
全局常量 C 类

5.8. 关于第三方库

Android 开发中不可避免要导入很多第三方库,这里我想谈谈我对第三方库的一些看法。关于第三方库的推荐我就不做介绍了,很多专门说这方面的文章。

5.8.1. 挑选第三方库的一些建议

  1. 项目中确实需要(这不是废话吗?用不着,我要它干嘛?呵呵,建议不要为了解决一个小小的问题导入一个大而全的库)
  2. 使用的人要多(大家都在用的一般更新会比较快,出现问题解决方案也多)
  3. 效率和体量的权衡(如果效率没有太大影响的情况下,我一般建议选择体量小点的,如,Gson vs Jackson,Gson 胜出;还是 65K 的问题)

5.8.2. 使用第三方库尽量二次封装

为什么要二次封装?
为了方便更换,说得稍微专业点为了降低耦合。
有很多原因可能需要你替换项目中的第三方库,这时候如果你是经过二次封装的,那么很简单,只需要在封装类中修改一下就可以了,完全不需要去全局检索代码。
我就遇到过几个替换第三方库的事情:

  1. 替换项目中的统计埋点工具
  2. 替换网络框架
  3. 替换日志工具

那该怎么封装呢?
一般的,如果是一些第三方的工具类,都会提供一些静态方法,那么这个就简单了,直接写一个工具类,提供类似的静态方法即可(就是用静态工厂模式)。
如下代码所示,这是对系统 Log 的简单封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Description: 企业中通用的Log管理
* 开发阶段LOGLEVEL = 6
* 发布阶段LOGLEVEL = -1
*/


public class Logger {

private static int LOGLEVEL = 6;
private static int VERBOSE = 1;
private static int DEBUG = 2;
private static int INFO = 3;
private static int WARN = 4;
private static int ERROR = 5;

public static void setDevelopMode(boolean flag) {
if(flag) {
LOGLEVEL = 6;
} else {
LOGLEVEL = -1;
}
}

public static void v(String tag, String msg) {
if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
Log.v(tag, msg);
}
}

public static void d(String tag, String msg) {
if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
Log.d(tag, msg);
}
}

public static void i(String tag, String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
Log.i(tag, msg);
}
}

public static void w(String tag, String msg) {
if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
Log.w(tag, msg);
}
}

public static void e(String tag, String msg) {
if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
Log.e(tag, msg);
}
}

}

现在如果想替换为 orhanobutLogger,那很简单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/**
* Description: 通用的Log管理工具类
* 开发阶段LOGLEVEL = 6
* 发布阶段LOGLEVEL = -1
*/


public class Logger {

public static String mTag = "MVPBestPractice";
private static int LOGLEVEL = 6;
private static int VERBOSE = 1;
private static int DEBUG = 2;
private static int INFO = 3;
private static int WARN = 4;
private static int ERROR = 5;

static {
com.orhanobut.logger.Logger
.init(mTag) // default PRETTYLOGGER or use just init()
.setMethodCount(3) // default 2
.hideThreadInfo() // default shown
.setLogLevel(LogLevel.FULL); // default LogLevel.FULL
}

public static void setDevelopMode(boolean flag) {
if(flag) {
LOGLEVEL = 6;
com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.FULL);
} else {
LOGLEVEL = -1;
com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.NONE);
}
}

public static void v(@NonNull String tag, String msg) {
if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.v(tag, msg);
com.orhanobut.logger.Logger.t(tag).v(msg);
}
}

public static void d(@NonNull String tag, String msg) {
if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.d(tag, msg);
com.orhanobut.logger.Logger.t(tag).d(msg);
}
}

public static void i(@NonNull String tag, String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.i(tag, msg);
com.orhanobut.logger.Logger.t(tag).i(msg);
}
}

public static void w(@NonNull String tag, String msg) {
if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.w(tag, msg);
com.orhanobut.logger.Logger.t(tag).w(msg);
}
}

public static void e(@NonNull String tag, String msg) {
if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.e(tag, msg);
com.orhanobut.logger.Logger.t(tag).e(msg);
}
}

public static void e(@NonNull String tag, Exception e) {
tag = checkTag(tag);
if(LOGLEVEL > ERROR) {
// Log.e(tag, e==null ? "未知错误" : e.getMessage());
com.orhanobut.logger.Logger.t(tag).e(e == null ? "未知错误" : e.getMessage());
}
}

public static void v(String msg) {
if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
// Log.v(mTag, msg);
com.orhanobut.logger.Logger.v(msg);
}
}

public static void d(String msg) {
if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
// Log.d(mTag, msg);
com.orhanobut.logger.Logger.d(msg);
}
}

public static void i(String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
// Log.i(mTag, msg);
com.orhanobut.logger.Logger.i(msg);
}
}

public static void w(String msg) {
if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
// Log.w(mTag, msg);
com.orhanobut.logger.Logger.v(msg);
}
}

public static void e(String msg) {
if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
// Log.e(mTag, msg);
com.orhanobut.logger.Logger.e(msg);
}
}

public static void e(Exception e) {
if(LOGLEVEL > ERROR) {
// Log.e(mTag, e==null ? "未知错误" : e.getMessage());
com.orhanobut.logger.Logger.e(e == null ? "未知错误" : e.getMessage());
}
}

public static void wtf(@NonNull String tag, String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.i(tag, msg);
com.orhanobut.logger.Logger.t(tag).wtf(msg);
}
}

public static void json(@NonNull String tag, String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.i(tag, msg);
com.orhanobut.logger.Logger.t(tag).json(msg);
}
}

public static void xml(@NonNull String tag, String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
tag = checkTag(tag);
// Log.i(tag, msg);
com.orhanobut.logger.Logger.t(tag).xml(msg);
}
}

public static void wtf(String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
// Log.i(tag, msg);
com.orhanobut.logger.Logger.wtf(msg);
}
}

public static void json(String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
// Log.i(tag, msg);
com.orhanobut.logger.Logger.json(msg);
}
}

public static void xml(String msg) {
if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
// Log.i(tag, msg);
com.orhanobut.logger.Logger.xml(msg);
}
}

private static String checkTag(String tag) {
if (TextUtils.isEmpty(tag)) {
tag = mTag;
}
return tag;
}

这里是最简单的一些替换,如果是替换网络框架,图片加载框架之类的,可能要多费点心思去封装一下,这里可以参考“门面模式”。(在这里就不展开来讲如何对第三库进行二次封装了,以后有时间专门写个帖子)

5.8.3. 建立单独的 Module 管理所有的第三库

原因前面已经说过了,而且操作也很简单。网上有不少拆分 Gradle 文件的方法,讲的都很不错。那我们就先从最简单的做起,赶快行动起来,把项目中用到的第三方库都集中到 Library Module 中来吧。

5.9. MVP vs MVVM

关于 MVP 和 MVVM 我只想说一句,它们并不是相斥的。具体它们是怎么不相斥的,markzhai 的这篇文章“MVPVM in Action, 谁告诉你MVP和MVVM是互斥的”说得很详细。

5.10. Code

抱歉,要食言了,AndroidStudio 出了点问题,代码还没写完,代码估计要这周末才能同步到 GitHub 上了,目前只上传了一个空框架。

5.11. 小结

历时三天的 MVP 总结,总算要告一段落了。前期断断续续地花了将近一周左右零散的时间去调研 MVP,直到正式开始码字的时候才发现准备的还不够。看了很多文章,有观点一致的,也有观点很不一致的。最关键的是,自己对于 MVP 还没有比较深刻的认知,所以在各种观点中取舍花了很长时间。
这算得上是我第一次真正意义上的写技术性的文章,说来惭愧,工作这么长时间了,现在才开始动笔。
总体来说,写得并不尽如人意,套一句老话——革命尚未成功,同志仍需努力。这算是一次尝试,希望以后会越写越顺畅。在这里给各位坚持看到此处的看官们问好了,祝大家一同进步。(欢迎大家围观我的GitHub,周末更新,会渐渐提交更多有用的代码的)

6. 进阶与不足

鉴于本人能力有限,还有很多想写的和该写的内容没有写出来,很多地方表达的也不是很清晰。下面说一说我觉得还有哪些不足和下一步要进阶的方向。

  1. 说好的“show me the code”,代码呢?(再次抱歉了)
  2. 上篇当中关于各种 Presenter 方案只是做了简单的罗列,并没有仔细分析各个方案的优点和不足
  3. 没有形成自己的框架(呵呵,好高骛远了,但是梦想还是要有的…)
  4. 没有单元测试(项目代码都还没有呢,提倡 TDD 不是,呵呵)
  5. 很多细节没有介绍清楚(如关于Model、Domain、Entity 等概念不是很清晰)
  6. 很多引用的观点没有指明出处(如有侵权,马上删除)
    ……

最后想说一句,没有完美的架构,没有完美的框架,赶紧编码吧!

7. 附录

Android MVP 总结资料汇总
附上我的思维导图:
MVPBestPractice.mmap
MVP总结.mmap
Presenter生命周期.mmap
怎么写Presenter.mmap

参考:

https://segmentfault.com/a/1190000003871577
http://www.open-open.com/lib/view/open1450008180500.html
http://www.myexception.cn/android/2004698.html
http://gold.xitu.io/entry/56cbf38771cfe40054eb3a34
http://kb.cnblogs.com/page/531834/
http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
http://www.open-open.com/lib/view/open1446377609317.html
http://my.oschina.net/mengshuai/blog/541314?fromerr=3J2TdbiW
http://gold.xitu.io/entry/56fcf1f75bbb50004d872e74
https://github.com/googlesamples/android-architecture/tree/todo-mvp-loaders/todoapp
http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
http://android.jobbole.com/82375/
http://blog.csdn.net/weizhiai12/article/details/47904135
http://android.jobbole.com/82051/
http://android.jobbole.com/81153/
http://blog.chengdazhi.com/index.php/115
http://blog.chengdazhi.com/index.php/131
http://www.codeceo.com/article/android-mvp-practice.html
http://www.wtoutiao.com/p/h01nn2.html
http://blog.jobbole.com/71209/
http://www.cnblogs.com/tianzhijiexian/p/4393722.html
https://github.com/xitu/gold-miner/blob/master/TODO/things-i-wish-i-knew-before-i-wrote-my-first-android-app.md
http://gold.xitu.io/entry/56cd79c12e958a69f944984c
http://blog.yongfengzhang.com/cn/blog/write-code-that-is-easy-to-delete-not-easy-to/
http://kb.cnblogs.com/page/533808/

坚持原创技术分享,您的支持将鼓励我继续创作!